图像转换

图形平滑

主要包括: 卷积/滤波/模糊/降噪

总结:

2D 卷积:均值滤波与模糊

关于滤波和模糊,很多人分不清:

卷积运算可看作是加权求和的过程,如要求某一个像素的卷积,那么需要将卷积核的中心锚点与此像素对齐,然后求此元素及其周边元素与卷积核的乘积之和,得到的值就是此像素的新的值。对于在边上的元素可以采用 padding 技术将其补齐,如下图:

img

对于卷积核有以下几个要求:

低通滤波器(LPF) 就是允许低频信号通过,在图像中边缘和噪点都相当于高频部分,所以低通滤波器用于去除噪点、平滑和模糊图像。高通滤波器(HPF)则反之,用来增强图像边缘,进行锐化处理。

常见噪声有椒盐噪声和高斯噪声,椒盐噪声可以理解为斑点,随机出现在图像中的黑点或白点;高斯噪声可以理解为拍摄图片时由于光照等原因造成的噪声;这样解释并不准确,只要能简单分辨即可

OpenCV提供的函数 cv.filter2D()可以对一幅图像进行卷积操作。均值滤波是一种最简单的滤波处理,它取的是卷积核区域内元素的均值,用cv2.blur() 实现,核说白了就是一个固定大小的数值数组。该数组带有一个 锚点 ,一般位于数组中央 ,如3×3的卷积核(其中因此的分母为权重矩阵所有元素的和):

【参考】

png

 

png

方框滤波

使用 cv2.boxFilter() 方框滤波跟均值滤波很像,当可选参数 normalize 为True的时候,方框滤波就是均值滤波。当 normalize 为 False 的时候,相当于区域内像素求和,当和大于 25 5时值就为 255,因此我们就看到下面的第三张图只有部分物体显示了出来,其他值都被置为了 255 。

png

高斯模糊(滤波)

现在把卷积核换成高斯核,简单的说方框不变,将原来每个方框的值是相等的,现在里面的值是符合高斯分布的,方框中心的值最大,其余方框根据距离中心元素的距离递减,构成一个高斯小山包,原来的求平均数变成求加权平均数,权就是方框里的值。如下图:

高斯滤波器是一个低通滤波器,可以使用 cv2.GaussianBlur() 来实现,参数如下:

也可以使用 cv2.getGaussianKernel()自己构建一个高斯核.

png

中值模糊

中值又叫中位数,是所有值排序后取中间的值。中值滤波就是用区域内的中值来代替本像素值,所以那种孤立的斑点,如0或255很容易消除掉,适用于去除椒盐噪声和斑点噪声。中值是一种非线性操作,效率相比前面几种线性滤波要慢。

使用 cv2.medianBlur() 完成操作

png

双边滤波

模糊操作基本都会损失掉图像细节信息,尤其前面介绍的线性滤波器,图像的边缘信息很难保留下来。然而,边缘edge信息是图像中很重要的一个特征,所以这才有了双边滤波。用cv2.bilateralFilter()函数实现,他能在保持边界清晰的情况下有效的去除噪音,但比较慢。

高斯滤波器只考虑像素之间的空间关系,而不会考虑像素之间的关系(像素的相似度),所以这种方法不会考虑一个像素是否位于边界,因此边界也会被模糊掉。

双边滤波在同时使用空间高斯权重灰度值相似性高斯权重。空间高斯函数确保只有邻近区的像素对中心点有影响,灰度值相似性高斯函数确保只有与中心像素灰度值相近的才会被用来做模糊运算。所以能保证边界不会被模糊,因此边界处的灰度值变化比较大。

png

形态学操作

形态学操作其实就是改变物体的形状,比如腐蚀就是”变瘦”,膨胀就是”变胖”,看下图就明白了:

img

一般情况下对二值化图像进行操作,来连接相邻的元素或分离成独立的元素。需要两个参数,一个是原始图像,第二个被称为结构化元素或者核,它是用来决定操作的性质的。基本操作为腐蚀和膨胀,腐蚀和膨胀是针对图片中的白色部分,他们的变体构成了开运算,闭运算,梯度等。

因为形态学操作其实也是应用卷积来实现的,所以结构元素也叫做核。结构元素可以是矩形/椭圆/十字形,可以用 cv2.getStructuringElement() 来生成不同形状的结构元素,比如:

img

【参考】

腐蚀

腐蚀的效果是把图片”变瘦”,其原理是在原图的小区域内取局部最小值。因为是二值化图,只有0和255,所以小区域内有一个是0该像素点就为0:

img

png

膨胀

膨胀与腐蚀相反,取的是局部最大值,效果是把图片”变胖”。一般在去噪音时先腐蚀再膨胀,因为腐蚀再去掉白噪音的同时,也会使前景对象变小,所以我们再膨胀。这时噪音已经被去除,不会再回来了,但是前景还在并会增加,膨胀也可以用来连接两个分开的物体。

膨胀使用 cv2.dilation() 操作,参数 iterations 表示膨胀在图片上的迭代次数。

png

开运算与闭运算

先进行腐蚀再进行膨胀就叫做开运算(因为先腐蚀会分开物体,这样容易记住),其作用是:分离物体,消除小区域,被用来去除噪音。

先膨胀再腐蚀成为闭运算(先膨胀会使白色的部分扩张,以至于消除/“闭合”物体里面的小黑洞,所以叫闭运算),被用来填充前景物体中的小洞,或者前景上的小黑点。 img 即:如果我们的目标物体外面有很多无关的小区域,就用开运算去除掉;如果物体内部有很多小黑洞,就用闭运算填充掉

函数可以使用cv2.morphotogyEx() , 参数为:

png

其他形态操作

形态梯度

膨胀图减去腐蚀图,dilation - erosion,这样会得到物体的轮廓。一样使用 cv2.morphologyEx() 来完成操作,只是第二个参数变成了cv2.MORPH_GRADIENT.

png

顶帽

原始图像与进行开运算之后得到的图像的差,src - opening, 使用 cv2.MORPH_TOPHAT

png

黑帽

闭运算后的图减去原图:closing - src, 使用 cv2.MORPH_BLACKHAT

png

图像梯度

图像梯度原理:简单来说就是求导。

如果你还记得高数中用一阶导数来求极值的话,就很容易理解了:把图片想象成连续函数,因为边缘部分的像素值是与旁边像素明显有区别的,所以对图片局部求极值,就可以得到整幅图片的边缘信息了。不过图片是二维的离散函数,导数就变成了差分,这个差分就称为图像的梯度。

【参考】

垂直边缘的提取

滤波是应用卷积来实现的,卷积的关键就是卷积核,我们来考察下面这个卷积核:

这个核是用来提取图片中的垂直边缘的,怎么做到的呢?看下图:

img 当前列左右两侧的元素进行差分,由于边缘的值明显小于(或大于)周边像素,所以边缘的差分结果会明显不同,这样就提取出了垂直边缘。同理,把上面那个矩阵转置一下,就是提取水平边缘。这种差分操作就叫图像的梯度计算:

png

Sobel算子

上面的这种差分方法就叫Sobel算子,它先在垂直方向计算梯度Gx=k1×src,再在水平方向计算梯度Gy=k2×src,最后求出总梯度:$G=\sqrt{Gx^2+Gy^2}$

Sobel算子是高斯平滑与微分操作的结合体,它的抗噪音能力很好。可以设定求导的方向(xorder或yorder)。还可以设定使用的卷积核的大小(ksize),如果ksize=-1,会使用3x3的Scharr滤波器,效果会更好,若速度相同,在使用3x3滤波器时尽量使用Scharr。

我们可以把前面的代码用Sobel算子更简单地实现:

很多人疑问,Sobel算子的卷积核这几个值是怎么来的呢?事实上,并没有规定,你可以用你自己的。当然,3×3下另外一个卷积核相比Sobel更好用,叫Scharr算子,大家可以了解下:$K = \left[ \begin{matrix} -3 & 0 & 3 \newline -10 & 0 & 10 \newline -3 & 0 & 3 \end{matrix} \right]$

Laplacian算子

高数中用一阶导数求极值,在这些极值的地方,二阶导数为0,所以可以通过求二阶导计算梯度:

可假设其离散实现类似于二阶Sobel导数,事实上OpenCV在计算拉普拉斯算子时直接调用Sobel算子。

png

边缘检测

使用 cv2.Canny() 来完成。实现步骤如下:

  1. 使用5×5高斯滤波消除噪声:边缘检测本身属于锐化操作,对噪点比较敏感,所以需要进行平滑处理。

  2. 计算图像梯度的方向:首先使用 Sobel 算子计算梯度 Gx 和 Gy ,然后算出梯度的方向:θ=arctan(Gy/Gx),保留这四个方向的梯度:0°/45°/90°/135°.梯度的方向一般总是与边界垂直。梯度方向被归为四类:垂直,水平,和两条对角线。

  3. 取局部极大值:梯度其实已经表示了轮廓,为了进一步筛选,可以在上面的四个角度方向上再取局部极大值:img

    比如,A点在45°方向上大于B/C点,那就保留它,把B/C设置为0。

  4. 滞后阈值:经过前面三步,就只剩下0和可能的边缘像素值了,为了最终确定下来,需要设定高低阈值:img

    • 像素点的值大于最高阈值,那肯定是边缘(上图A)
    • 同理像素值小于最低阈值,那肯定不是边缘
    • 像素值介于两者之间,如果与高于最高阈值的点连接,也算边缘,所以上图中C算,B不算

Canny推荐的高低阈值比在2:1到3:1之间。其实很多情况下,阈值分割后再检测边缘,效果会更好.

png

轮廓

轮廓是一系列相连的点组成的曲线,具有相同的颜色或者灰度,代表了物体的基本外形。

谈起轮廓不免想到边缘,它们确实很像。简单的说,轮廓是连续的,边缘并不全都连续(下图)。其实边缘主要是作为图像的特征使用,比如可以用边缘特征可以区分脸和手,而轮廓主要用来分析物体的形态,比如物体的周长和面积等,可以说边缘包括轮廓。

img

寻找轮廓的操作一般用于二值化图,所以通常会使用阈值分割或Canny边缘检测先得到二值图。

** 寻找轮廓是针对白色物体的,一定要保证物体是白色,而背景是黑色,不然很多人在寻找轮廓时会找到图片最外面的一个框 **

轮廓基础

寻找轮廓

使用 cv2.findContours() 寻找轮廓,返回图像、轮廓、层级,参数如下:

png

绘制轮廓

轮廓找出来后,为了方便观看,可以像前面图中那样用红色画出来:cv2.drawContours(),此函数的参数为:

png

轮廓特征

要学习轮廓的特征首先要找到轮廓。使用上面龙猫与花的轮廓。

png

计算

面积

注意轮廓特征计算的结果并不等同于像素点的个数,而是根据几何方法算出来的,所以有小数。

如果统计二值图中像素点个数,应尽量避免循环,可以使用 cv2.countNonZero(),更加高效

轮廓周长

图像矩

矩可以理解为图像的各类几何特征:M中包含了很多轮廓的特征信息,比如M['m00']表示轮廓面积,与前面cv2.contourArea()计算结果是一样的。质心也可以用算:cx, cy = M['m10'] / M['m00'], M['m01'] / M['m00']

外接矩形

形状的外接矩形有两种,如下图,绿色的叫外接矩形,表示不考虑旋转并且能包含整个轮廓的矩形。蓝色的叫最小外接矩,考虑了旋转:

img

png

最小外接圆

外接圆跟外接矩形一样,找到一个能包围物体的最小圆:

png

拟合椭圆

png

轮廓相似

cv2.matchShapes()可以检测两个形状之间的相似度,返回值越小,越相似。先读入下面这张图片:img

轮廓的层级

前面我们使用 cv2.findContours() 寻找轮廓时,参数3表示轮廓的寻找方式(RetrievalModes),当时我们传入的是 cv2.RETR_TREE,它表示什么意思呢?另外,函数返回值 hierarchy 有什么用途呢?

【参考】

理解轮廓层级

很多情况下,图像中的形状之间是有关联的,比如说下图:

img

图中总共有8条轮廓,2和2a分别表示外层和里层的轮廓,3和3a也是一样。从图中看得出来:

这里面OpenCV关注的就是两个概念:同一轮廓等级和轮廓间的子属关系。

OpenCV中轮廓等级的表示

如果我们打印出 cv2.findContours() 函数的返回值hierarchy,会发现它是一个包含4个值的数组:[Next, Previous, First Child, Parent]

举例来说,前面图中跟0处于同一层级的下一条轮廓是1,所以Next=1;同理,对轮廓1来说,Next=2;那么对于轮廓2呢?没有与它同一层级的下一条轮廓了,此时Next=-1

跟前面一样,对于轮廓1来说,Previous=0;对于轮廓2,Previous=1;对于轮廓1,没有上一条轮廓了,所以Previous=-1。

比如对于轮廓2,第一条子轮廓就是轮廓2a,所以First Child=2a;对轮廓3a,First Child=4。

比如2a的父轮廓是2,Parent=2;轮廓2没有父轮廓,所以Parent=-1。

png

png

由上面可以看出,轮廓的顺序为

image

轮廓寻找方式

OpenCV中有四种轮廓寻找方式RetrievalModes,下面分别来看下:

RETR_LIST(retrieves list)

这是最简单的一种寻找方式,它不建立轮廓间的子属关系,也就是所有轮廓都属于同一层级。这样的话,hierarchy中的后两个值[Next, Previous, First Child, Parent] 都为-1。因为没有从属关系,所以轮廓0的下一条是1,1的下一条是2……。如果你不需要轮廓层级信息的话,cv2.RETR_LIST更推荐使用,因为性能更好。比如同样的图,我们使用RETR_LIST来寻找轮廓

RETR_TREE

cv2.RETR_TREE就是之前我们一直在使用的方式,它会完整建立轮廓的层级从属关系。

RETR_EXTERNAL

这种方式只寻找最高层级的轮廓,即最外层的轮廓,也就是它只会找到前面我们所说的3条0级轮廓:

png

RETR_CCOMP

相比之下cv2.RETR_CCOMP比较难理解,但其实也很简单:它把所有的轮廓只分为2个层级,不是外层的就是里层的。

png

将上面结果绘制在同一张图上如下:

image

寻找轮廓并填充

寻找轮廓并为最里层的轮廓添加填充,效果如下:

img

png

凸包及其其他轮廓特征

【参考】

多边形逼近

前面使用过矩形和最小矩形来包围花朵,我们还可以使用多边形来包围花朵。这里可以使用 cv2.approxPolyDP() , 参数有:

画线使用 cv2.polylines() ,参数如下:

png

凸包

凸包跟多边形逼近很像,只不过它是物体最外层的”凸”多边形:集合A内连接任意两个点的直线都在A的内部,则称集合A是凸形的。如下图,红色的部分为手掌的凸包,双箭头部分表示凸缺陷(Convexity Defects),凸缺陷常用来进行手势识别等:

img

使用 cv2.convexHull 来寻找凸包的角点, 他有个可选参数returnPoints,默认是True,代表返回角点的x/y坐标;如果为False的话,表示返回轮廓中是凸包角点的索引。

当使用 cv2.convexityDefects() 计算凸包缺陷时,cv2.convexHull 的returnPoints需为False 。cv2.convexityDefects()的返回值为:

也可以使用 cv2.isContourConvex 判断是不是凸包。

png

点到轮廓的距离

使用 cv2.pointPolygonTest() 函数计算点到轮廓的最短距离(也就是垂线),又称多边形测试. 函数的第三个参数为True时表示计算距离值:点在轮廓外面值为负,点在轮廓上值为0,点在轮廓里面值为正;参数3为False时,只返回-1/0/1表示点相对轮廓的位置,不计算距离。